package com.shuang.bai.simple.utils;

import java.io.Serializable;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

import org.apache.commons.lang3.Validate;

import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyObject;

/**
 * a method name tool to get the method name or method name remove "get"
 * 
 * @author Shuang Bai
 *
 */
public class MethodNames {
	private static final Map<Class<?>, Object> proxyCache = new ConcurrentHashMap<>();

	private static ThreadLocal<String> methodNameThreadLocal = new ThreadLocal<String>();

	public static <T> String methodName(Class<T> clazz, Function<T, ?> fun) {
		@SuppressWarnings({ "unchecked" })
		T t = (T) proxyCache.computeIfAbsent(clazz, MethodNames::createProxy);

		fun.apply(t);
		String methodName = methodNameThreadLocal.get();
		methodNameThreadLocal.remove();
		if (methodName == null) {
			throw new IllegalArgumentException(
					"parameter can't be final method, the this method in class:" + clazz.getName());
		}
		return methodName;
	}

	/**
	 * be careful to use this method, because it is a little slow, right now no
	 * solution to make it fast, {@link #methodName(Class,Function)} is faster
	 * than this method
	 * 
	 * @see MethodNames#methodName(Class,Function)
	 * @param fun
	 * @return
	 */
	public static <T> String methodName(SerilizableFun<T, ?> fun) {
		Class<T> clazz = getClassByFunction(fun);
		return methodName(clazz, fun);
	}

	/**
	 * be careful to use this method, because it is a little slow, right now no
	 * solution to make it fast,{@link #fieldName(Class,Function,Function)} is
	 * faster than this method
	 * 
	 * @see MethodNames#fieldName(Class,Function)
	 * 
	 * @param fun
	 * @return
	 */
	public static <T> String fieldName(SerilizableFun<T, ?> fun) {
		Class<T> clazz = getClassByFunction(fun);
		return baseFieldNames(clazz, fun);
	}

	/**
	 * be careful to use this method,because it is a little slow, right now no
	 * solution to make it fast, {@link #fieldName(Class,Function,Function)} is
	 * faster than this method
	 * 
	 * @see MethodNames#methodName(Class,Function,Function)
	 * @param fun
	 * @return
	 */
	public static <T, U> String fieldName(SerilizableFun<T, U> fun1, Function<U, ?> fun2) {
		Class<T> clazz = getClassByFunction(fun1);
		return baseFieldNames(clazz, fun1, fun2);
	}

	public static <T, U, D> String fieldName(SerilizableFun<T, U> fun1, Function<U, D> fun2, Function<D, ?> fun3) {
		Class<T> clazz = getClassByFunction(fun1);
		return baseFieldNames(clazz, fun1, fun2, fun3);
	}

	public static <T, U, D, H> String fieldName(SerilizableFun<T, U> fun1, Function<U, D> fun2, Function<D, H> fun3,
			Function<H, ?> fun4) {
		Class<T> clazz = getClassByFunction(fun1);
		return baseFieldNames(clazz, fun1, fun2, fun3, fun4);
	}

	public static <T, U, D, H, J> String fieldName(SerilizableFun<T, U> fun1, Function<U, D> fun2, Function<D, H> fun3,
			Function<H, J> fun4, Function<J, ?> fun5) {
		Class<T> clazz = getClassByFunction(fun1);
		return baseFieldNames(clazz, fun1, fun2, fun3, fun4, fun5);
	}

	public static <T, U, D, H, J, I> String fieldName(SerilizableFun<T, U> fun1, Function<U, D> fun2,
			Function<D, H> fun3, Function<H, J> fun4, Function<J, I> fun5, Function<I, ?> fun6) {
		Class<T> clazz = getClassByFunction(fun1);
		return baseFieldNames(clazz, fun1, fun2, fun3, fun4, fun5, fun6);
	}

	public static <T> String fieldName(Class<T> clazz, Function<T, ?> fun1) {
		return baseFieldNames(clazz, fun1);
	}

	public static <T, U> String fieldName(Class<T> clazz, Function<T, U> fun1, Function<U, ?> fun2) {
		return baseFieldNames(clazz, fun1, fun2);
	}

	public static <T, U, D> String fieldName(Class<T> clazz, Function<T, U> fun1, Function<U, D> fun2,
			Function<D, ?> fun3) {
		return baseFieldNames(clazz, fun1, fun2, fun3);
	}

	public static <T, U, D, H> String fieldName(Class<T> clazz, Function<T, U> fun1, Function<U, D> fun2,
			Function<D, H> fun3, Function<H, ?> fun4) {
		return baseFieldNames(clazz, fun1, fun2, fun3, fun4);
	}

	public static <T, U, D, H, J> String fieldName(Class<T> clazz, Function<T, U> fun1, Function<U, D> fun2,
			Function<D, H> fun3, Function<H, J> fun4, Function<J, ?> fun5) {
		return baseFieldNames(clazz, fun1, fun2, fun3, fun4, fun5);
	}

	public static <T, U, D, H, J, I> String fieldName(Class<T> clazz, Function<T, U> fun1, Function<U, D> fun2,
			Function<D, H> fun3, Function<H, J> fun4, Function<J, I> fun5, Function<I, ?> fun6) {
		return baseFieldNames(clazz, fun1, fun2, fun3, fun4, fun5, fun6);
	}

	@SuppressWarnings("unchecked")
	private static <T> List<String> baseMethodNames(Class<T> clazz, Function<T, ?> fun, Function<?, ?>... funs) {
		List<String> methodNames = new ArrayList<>();
		String methodName = methodName(clazz, fun);
		methodNames.add(methodName);
		Class<Object> returnType = (Class<Object>) clazz;
		try {
			for (Function<?, ?> f : funs) {
				returnType = (Class<Object>) returnType.getMethod(methodName).getReturnType();
				Function<Object, ?> fn = (Function<Object, ?>) f;
				methodName = methodName(returnType, fn);
				methodNames.add(methodName);
			}

		} catch (NoSuchMethodException | SecurityException e) {
			// impossible
			throw new RuntimeException("reflect attrive method return type fail", e);
		}
		return methodNames;
	}

	@SuppressWarnings("unchecked")
	private static <T> T createProxy(Class<T> clazz) {
		if (Modifier.isFinal(clazz.getModifiers())) {
			throw new IllegalArgumentException("class is final, right now not support final class: " + clazz);
		}
		ProxyFactory proxyFactory = new ProxyFactory();
		if (clazz.isInterface()) {
			proxyFactory.setInterfaces(new Class[] { clazz });
		} else {
			proxyFactory.setSuperclass(clazz);
		}
		ProxyObject proxyObject;
		MethodHandler methodHandler;
		try {
			proxyObject = (ProxyObject) proxyFactory.create(null, null);
			methodHandler = (Object self, Method thisMethod, Method proceed, Object[] args) -> intercept(thisMethod);
		} catch (NoSuchMethodException | IllegalArgumentException | InstantiationException | IllegalAccessException
				| InvocationTargetException e) {
			// impossible
			throw new RuntimeException("create proxy object fail", e);
		}
		proxyObject.setHandler(methodHandler);
		return (T) proxyObject;
	}

	private static Object intercept(Method method) throws Throwable {
		methodNameThreadLocal.set(method.getName());
		Class<?> returnType = method.getReturnType();
		// to avoid ClassCastException, return default value for every type
		if (!returnType.isPrimitive()) {
			return null;
		} else if (returnType == byte.class) {
			return (byte) 0;
		} else if (returnType == short.class) {
			return (short) 0;
		} else if (returnType == char.class) {
			return '0';
		} else if (returnType == int.class) {
			return 0;
		} else if (returnType == long.class) {
			return 0L;
		} else if (returnType == float.class) {
			return 0.0F;
		} else if (returnType == double.class) {
			return 0.0;
		} else if (returnType == boolean.class) {
			return false;
		} else {
			return null;
		}
	}

	/**
	 * deduce class of Function belongs
	 * 
	 * @param fun
	 * @return
	 */
	private static <T> Class<T> getClassByFunction(SerilizableFun<T, ?> fun) {
		SerializedLambda serializedLambda = null;
		try {
			Method writeReplace = fun.getClass().getDeclaredMethod("writeReplace");
			writeReplace.setAccessible(true);
			serializedLambda = (SerializedLambda) writeReplace.invoke(fun);
		} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
				| InvocationTargetException e) {
			// impossible
			throw new RuntimeException("deduce Class name of SerializableFun with writeReplace fail", e);
		}
		String implMethodSignature = serializedLambda.getImplMethodSignature();
		String className = implMethodSignature.substring(2, implMethodSignature.indexOf(";)")).replace('/', '.');
		try {
			@SuppressWarnings("unchecked")
			Class<T> clazz = (Class<T>) Class.forName(className);
			return clazz;
		} catch (ClassNotFoundException e) {
			// impossible
			throw new RuntimeException("create Class with Class.forName(className) fail , className:" + className, e);
		}
	}

	/**
	 * deduce all the method names, this base method be called by other method,
	 * but never public this method, because we want compiler check
	 * 
	 * @param clazz
	 * @param fun
	 * @param funs
	 * @return
	 */
	private static <T> String baseFieldNames(Class<T> clazz, Function<T, ?> fun, Function<?, ?>... funs) {
		return baseMethodNames(clazz, fun, funs).stream()//
				.peek(it -> {
					Validate.isTrue(it.startsWith("get"), "method name must starts with \"get\", this method:%", it);
				})//
				.map(it -> it.substring(3))//
				.filter(it -> !it.isEmpty())//
				.reduce((current, next) -> current + "." + next)//
				.get();
	}

	@FunctionalInterface
	public static interface SerilizableFun<T, D> extends Function<T, D>, Serializable {
	}
}
